package com.inspectortime.domain.impl;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.annotations.Where;

import com.inspectortime.common.IdentifiableUtils;
import com.inspectortime.common.PersistentEntityBase;
import com.inspectortime.domain.EmailTemplate;
import com.inspectortime.domain.Inspection;
import com.inspectortime.domain.Inspector;
import com.inspectortime.domain.InspectorCompany;
import com.inspectortime.domain.OptionalService;
import com.inspectortime.domain.RealEstateAgent;
import com.inspectortime.domain.Scheduler;
import com.inspectortime.domain.User;
import com.inspectortime.domain.pricing.BaseFeeWithAdjustersCalculator;
import com.inspectortime.domain.pricing.FeeCalculator;
import com.inspectortime.domain.pricing.PriceAdjusterException;
import com.inspectortime.domain.pricing.PricingPlanType;
import com.inspectortime.domain.type.Address;
import com.inspectortime.timestamp.Timestamped;

@Entity
@Table(name = "INSPECTOR_COMPANY")
public class InspectorCompanyImpl extends PersistentEntityBase implements
		InspectorCompany, Timestamped {

	private static Log log = LogFactory.getLog(InspectorCompanyImpl.class);

	private Address address;
	private double baseInspectionFee;
	private String companyPhone;
	private boolean paypalEnabled;
	private String paypalPaymentEmail;

	private Date createdTime;
	private String distanceInMilesCenterZip;

	private byte[] logoImage = new byte[0];

	private double distanceInMilesFeeIncreasePerMile;

	private int distanceInMilesLimit;
	
	private List<EmailTemplate> emailTemplates = new ArrayList<EmailTemplate>();

	private List<FeeCalculator> feeCalculators = new ArrayList<FeeCalculator>();

	private Collection<Inspector> inspectors = new ArrayList<Inspector>();

	private Date lastModifiedTime;
	private int maxMilesServed = 30;
	private String name;
	private List<OptionalService> optionalServices = new ArrayList<OptionalService>();
	private PricingPlanType pricingPlanType;
	private String recipientToken;
	private Collection<RealEstateAgent> referringAgents = new ArrayList<RealEstateAgent>();
	private String refundToken;
	private Collection<Scheduler> schedulers = new ArrayList<Scheduler>();
	private String shortName;
	private String simLoginId;
	private String simTransactionKey;

	public InspectorCompanyImpl() {
	}

	public double calculateInspectionFee(Inspection inspection)
			throws PriceAdjusterException {

		// Default fee
		double fee = this.getBaseInspectionFee();

		// Split calculators into those that define specific zip codes, and
		// those that don't
		List<FeeCalculator> specificCalculators = new ArrayList<FeeCalculator>();
		List<FeeCalculator> generalCalculators = new ArrayList<FeeCalculator>();
		for (FeeCalculator calculator : this.getFeeCalculators()) {
			if (calculator.hasSupportedZipCodes()) {
				specificCalculators.add(calculator);
			} else {
				generalCalculators.add(calculator);
			}
		}

		// Then try to calculate using the zip-specific calculators first
		boolean match = false;
		for (FeeCalculator calculator : specificCalculators) {
			if (calculator.supports(inspection)) {
				fee = calculator.calculateFee(inspection);
				match = true;
			}
		}

		// Then optionally try the zip-generic calculators
		if (!match) {
			for (FeeCalculator calculator : generalCalculators) {
				if (calculator.supports(inspection)) {
					fee = calculator.calculateFee(inspection);
				}
			}
		}

		log
				.debug("Calculated inspection fee before global adjustments: "
						+ fee);

		// Adjust using global adjustment settings

		// 1. Driving distance
		if (this.getDistanceInMilesLimit() > 0
				&& this.getDistanceInMilesCenterZip() != null
				&& this.getDistanceInMilesCenterZip().length() == 5) {
			double dist = getServices()
					.getZipCodeSearchService()
					.findDistanceInMiles(this.getDistanceInMilesCenterZip(),
							inspection.getProperty().getLocation().getZipCode());
			log.debug("Distance from " + this.getDistanceInMilesCenterZip()
					+ " to "
					+ inspection.getProperty().getLocation().getZipCode()
					+ ": " + dist);
			double distOverLimit = dist - this.getDistanceInMilesLimit();
			if (distOverLimit > 0) {
				fee += distOverLimit
						* this.getDistanceInMilesFeeIncreasePerMile();
			}
		}

		log.debug("Calculated inspection fee after global adjustments: " + fee);

		// Add optional services
		double optionalServicesFee = 0;
		for (OptionalService service :  inspection.getOptionalServices()) {
			optionalServicesFee += service.getPrice();
		}
		fee += optionalServicesFee;
		log.debug("Added optional services for $" + optionalServicesFee
				+ ", total inspection fee $" + fee);

		return fee;
	}

	public InspectorCompanyImpl(Address address) {
		this.address = address;
	}

	public void addInspector(Inspector inspector) {
		inspectors.add(inspector);
	}

	public void addScheduler(Scheduler obj) {
		schedulers.add(obj);
	}

	public FeeCalculator findFeeCalculatorById(String id) {
		return (FeeCalculator) IdentifiableUtils.findById(this
				.getFeeCalculators(), id);
	}

	public List<Inspection> findInspectionsBetween(Date start, Date end) {
		// TODO: May want to sort them, or implement using only one query
		List<Inspection> inspectionsAcrossInspectors = new ArrayList<Inspection>();
		for (Inspector inspector : this.getInspectors()) {
			inspectionsAcrossInspectors.addAll(inspector
					.findInspectionsBetween(start, end));
		}
		return inspectionsAcrossInspectors;
	}

	public OptionalService findOptionalServiceById(String id) {
		return (OptionalService) IdentifiableUtils.findById(this
				.getOptionalServices(), id);
	}

	@Embedded
	public Address getAddress() {
		return address;
	}

	public double getBaseInspectionFee() {
		return baseInspectionFee;
	}

	public Date getCreatedTime() {
		return createdTime;
	}

	public String getDistanceInMilesCenterZip() {
		return distanceInMilesCenterZip;
	}

	public double getDistanceInMilesFeeIncreasePerMile() {
		return distanceInMilesFeeIncreasePerMile;
	}

	public int getDistanceInMilesLimit() {
		return distanceInMilesLimit;
	}

	@OneToMany(mappedBy = "company", targetEntity = EmailTemplateImpl.class, cascade = { CascadeType.ALL })
	public List<EmailTemplate> getEmailTemplates() {
		return emailTemplates;
	}

	@Transient
	public Collection<User> getEmployees() {
		Collection<User> coll = new ArrayList<User>();
		coll.addAll(this.getInspectors());
		coll.addAll(this.getSchedulers());
		return coll;
	}

	@OneToMany(mappedBy = "inspectorCompany", targetEntity = BaseFeeWithAdjustersCalculator.class, cascade = { CascadeType.ALL })
	/* @JoinColumn(name = "inspectorCompany_id") */
	public List<FeeCalculator> getFeeCalculators() {
		return feeCalculators;
	}

	@OneToMany(mappedBy = "company", targetEntity = InspectorImpl.class, cascade = { CascadeType.ALL })
	@Where(clause = "status <> 1")
	public Collection<Inspector> getInspectors() {
		return inspectors;
	}

	public Date getLastModifiedTime() {
		return lastModifiedTime;
	}

	public int getMaxMilesServed() {
		return maxMilesServed;
	}

	@Column(length = 100)
	public String getName() {
		return name;
	}

	@OneToMany(mappedBy = "company", targetEntity = OptionalServiceImpl.class, cascade = { CascadeType.ALL })
	public List<OptionalService> getOptionalServices() {
		return optionalServices;
	}

	public PricingPlanType getPricingPlanType() {
		return pricingPlanType;
	}

	@Column(length = 100)
	public String getRecipientToken() {
		return recipientToken;
	}

	@ManyToMany(targetEntity = RealEstateAgentImpl.class, cascade = { CascadeType.ALL })
	public Collection<RealEstateAgent> getReferringAgents() {
		return referringAgents;
	}

	@Column(length = 100)
	public String getRefundToken() {
		return refundToken;
	}

	@OneToMany(mappedBy = "company", targetEntity = SchedulerImpl.class, cascade = { CascadeType.ALL })
	@Where(clause = "status <> 1")
	public Collection<Scheduler> getSchedulers() {
		return schedulers;
	}

	@Column(length = 50)
	public String getShortName() {
		return shortName;
	}

	public String getSimLoginId() {
		return simLoginId;
	}

	public String getSimTransactionKey() {
		return simTransactionKey;
	}

	public void removeFeeCalculator(String id) {
		IdentifiableUtils.removeById(this.feeCalculators, id);
	}

	public void removeOptionalService(String id) {
		IdentifiableUtils.removeById(this.optionalServices, id);
	}

	public void setAddress(Address address) {
		this.address = address;
	}

	public void setBaseInspectionFee(double baseInspectionFee) {
		this.baseInspectionFee = baseInspectionFee;
	}

	public void setCreatedTime(Date createdTime) {
		this.createdTime = createdTime;
	}

	public void setDistanceInMilesCenterZip(String distanceInMilesCenterZip) {
		this.distanceInMilesCenterZip = distanceInMilesCenterZip;
	}

	public void setDistanceInMilesFeeIncreasePerMile(
			double distanceInMilesFeeIncreasePerMile) {
		this.distanceInMilesFeeIncreasePerMile = distanceInMilesFeeIncreasePerMile;
	}

	public void setDistanceInMilesLimit(int distanceInMilesLimit) {
		this.distanceInMilesLimit = distanceInMilesLimit;
	}

	public void setEmailTemplates(List<EmailTemplate> l) {
		this.emailTemplates = l;
	}

	public void setFeeCalculators(List<FeeCalculator> a) {
		this.feeCalculators = a;
	}

	public void setInspectors(Collection<Inspector> coll) {
		this.inspectors = coll;
	}

	public void setLastModifiedTime(Date lastModifiedTime) {
		this.lastModifiedTime = lastModifiedTime;
	}

	public void setMaxMilesServed(int maxMilesServed) {
		this.maxMilesServed = maxMilesServed;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setOptionalServices(List<OptionalService> optionalServices) {
		this.optionalServices = optionalServices;
	}

	public void setPricingPlanType(PricingPlanType pricingPlanType) {
		this.pricingPlanType = pricingPlanType;
	}

	public void setRecipientToken(String recipientToken) {
		this.recipientToken = recipientToken;
	}

	public void setReferringAgents(Collection<RealEstateAgent> agents) {
		this.referringAgents = agents;
	}

	public void setRefundToken(String refundToken) {
		this.refundToken = refundToken;
	}

	public void setSchedulers(Collection<Scheduler> coll) {
		this.schedulers = coll;
	}

	public void setShortName(String shortName) {
		this.shortName = shortName;
	}

	public void setSimLoginId(String simLoginId) {
		this.simLoginId = simLoginId;
	}

	public void setSimTransactionKey(String simTransactionKey) {
		this.simTransactionKey = simTransactionKey;
	}

	@Column(length = 131072)
	public byte[] getLogoImage() {
		return logoImage;
	}

	public void setLogoImage(byte[] logoFile) {
		this.logoImage = logoFile;
	}

	public void readLogoImageFrom(InputStream in) throws IOException {
		setLogoImage(IOUtils.toByteArray(in));
	}

	public void writeLogoImageTo(OutputStream out) throws IOException {
		IOUtils.write(getLogoImage(), out);
	}

	@Column(length = 20)
	public String getCompanyPhone() {
		return companyPhone;
	}

	public void setCompanyPhone(String companyPhone) {
		this.companyPhone = companyPhone;
	}

	public boolean isPaypalEnabled() {
		return paypalEnabled;
	}

	public void setPaypalEnabled(boolean paymentEnabled) {
		this.paypalEnabled = paymentEnabled;
	}

	@Column(length = 100)
	public String getPaypalPaymentEmail() {
		return paypalPaymentEmail;
	}

	public void setPaypalPaymentEmail(String paymentEmail) {
		this.paypalPaymentEmail = paymentEmail;
	}


}
